package org.eclipse.iota.evaluator;

import org.eclipse.iota.assignments.Assignment;
import org.eclipse.iota.model.Addition;
import org.eclipse.iota.model.Boolean;
import org.eclipse.iota.model.Constant;
import org.eclipse.iota.model.Constraint;
import org.eclipse.iota.model.Division;
import org.eclipse.iota.model.Equation;
import org.eclipse.iota.model.Exponentiation;
import org.eclipse.iota.model.Expression;
import org.eclipse.iota.model.Multiplication;
import org.eclipse.iota.model.Negation;
import org.eclipse.iota.model.Reference;
import org.eclipse.iota.model.Variable;

/**
 * An evaluator for constant values. Based on an assignment.
 * @author Christian Krause
 * @generated NOT
 */
public class IotaConstantEvaluator implements IotaEvaluator<Constant,Boolean> {
	
	// Assignment to be used.
	private Assignment assignment;
	
	// Modulus to be used.
	private Constant modulus;
	
	/**
	 * Default constructor.
	 */
	public IotaConstantEvaluator() {
		// Empty assignment.
		this(new Assignment());
	}
	
	/**
	 * Alternative constructor.
	 * @param assignment Assignment to be used.
	 */
	public IotaConstantEvaluator(Assignment assignment) {
		this.assignment = assignment;
	}
	
	
	/**
	 * Evaluate an expression.
	 * @param exp Expression to be evaluated.
	 * @return Result of the evaluation.
	 * @throws EvaluationException If something goes wrong.
	 */
	public Constant evaluate(Expression expression) throws EvaluationException {
		return evaluate(expression, true);
	}
	
	/*
	 * Evaluate an expression.
	 */
	protected Constant evaluate(Expression exp, boolean check) throws EvaluationException {
		
		// Basic cases.
		if (exp instanceof Constant) {
			Constant result = ((Constant) exp).copy();
			return checkModulus(result, check);
		}
		if (exp instanceof Reference) {
			Variable variable = ((Reference) exp).getVariable();
			if (assignment.get(variable) instanceof Constant) {
				Constant result = ((Constant) assignment.get(variable)).copy();
				return checkModulus(result, check);
			} else {
				throw new EvaluationException(exp + " is not mapped to a constant");
			}
		}
		
		// Operations.
		if (exp instanceof Addition) return evaluate((Addition) exp, check);
		if (exp instanceof Negation) return evaluate((Negation) exp, check);
		if (exp instanceof Multiplication) return evaluate((Multiplication) exp, check);
		if (exp instanceof Division) return evaluate((Division) exp, check);
		if (exp instanceof Exponentiation) return evaluate((Exponentiation) exp, check);
		
		// Unknown expression type.
		throw new EvaluationException("Unknow expression type: " + exp.eClass().getName());
		
	}
	
	/**
	 * Evaluate an addition.
	 * @param addition Addition.
	 * @return Evaluation result.
	 */
	protected Constant evaluate(Addition addition, boolean check) throws EvaluationException {
		Constant result = new Constant(0);
		for (Expression operand : addition.getOperands()) {
			result.add( evaluate(operand, check) );
		}
		return checkModulus(result, check);
	}
	
	/**
	 * Evaluate a negation.
	 * @param negation Negation.
	 * @return Evaluation result.
	 */
	protected Constant evaluate(Negation negation, boolean check) throws EvaluationException {
		Constant result = evaluate(negation.getOperand(), check).negate();
		return checkModulus(result, check);
	}
	
	/**
	 * Evaluate a multiplication.
	 * @param multiplication Multiplication.
	 * @return Evaluation result.
	 */
	protected Constant evaluate(Multiplication multiplication, boolean check) throws EvaluationException {
		Constant result = new Constant(1);
		for (Expression operand : multiplication.getOperands()) {
			result.multiply( evaluate(operand, check) );
			result = checkModulus(result, check);
		}
		return checkModulus(result, check);
	}
	
	/**
	 * Evaluate a division.
	 * @param division Division.
	 * @return Evaluation result.
	 */
	protected Constant evaluate(Division division, boolean check) throws EvaluationException {
		Constant left = evaluate(division.getLeft(), check);
		Constant right = evaluate(division.getRight(), check);
		if (right.isZero() || !left.copy().mod(right.copy().abs()).isZero()) {
			throw new EvaluationException("Cannot divide " + left + " by " + right + " in " + division);
		}
		return checkModulus(left.divide(right), check);
	}
	
	/**
	 * Evaluate an exponentiation.
	 * @param exponentiation Exponentiation.
	 * @return Evaluation result.
	 */
	protected Constant evaluate(Exponentiation exp, boolean check) throws EvaluationException {
		
		// Check if the exponent is negative or too big.
		Constant exponent = evaluate(exp.getRight(), false);
		if (exponent.isNegative()) throw new EvaluationException("Negative exponent: " + exponent);
		if (!exponent.isShort()) throw new EvaluationException("Exponent too big: " + exponent);
		
		Constant base = evaluate(exp.getLeft(), check);
		if (modulus!=null && check) {
			return base.modPow(exponent, modulus);
		} else {
			return base.pow(exponent.getValue().intValue());
		}
	}
	
	
	/**
	 * Evaluate a constraint.
	 * @param constraint Constraint to be evaluated.
	 * @return the boolean result.
	 * @throws EvaluationException On evaluation errors.
	 */
	public Boolean evaluate(Constraint constraint) throws EvaluationException {
		
		// Equations.
		if (constraint instanceof Equation) {
			Equation equation = (Equation) constraint;
			Constant left = evaluate(equation.getLeft());
			Constant right = evaluate(equation.getRight());
			switch (equation.getType()) {
				case EQUAL:		return new Boolean(left.isEqualTo(right));
				case LESS:		return new Boolean(left.isLessThan(right));
				case GREATER:	return new Boolean(left.isGreaterThan(right));
				case NOT_EQUAL:	return new Boolean(!left.isEqualTo(right));
				default: 		throw new EvaluationException("Unknown equation type: '" + equation.getType() + "'");
			}
		}
		
		// Booleans.
		else if (constraint instanceof Boolean) {
			return new Boolean(((Boolean) constraint).getValue());
		}
		
		// Unknown constraint type.
		else {
			throw new EvaluationException("Constraint has unknown type: " + constraint);
		}
		
	}
	
	/**
	 * Set the assignment to be used.
	 * @param assignment Assignment
	 */
	public void setAssignment(Assignment assignment) {
		this.assignment = assignment;		
	}
	
	/**
	 * Set the modulus to be used.
	 * @param modulus Modulus.
	 */
	public void setModulus(Constant modulus) {
		this.modulus = modulus;
	}
	
	/*
	 * Private helper for converting a constant according to the current modulus.
	 */
	private Constant checkModulus(Constant constant, boolean check) {
		if (check && modulus!=null) {
			return constant.copy().mod(modulus);
		}
		return constant;
	}

	public Assignment getAssignment() {
		return assignment;
	}
	
}
